/* Bento Studio / 2012 / Uniflow / v1.1a
 *
 * Uniflow is an Unity clone of a well-known 3D gallery UI.
 *
 * UniflowGallery:
 * This component creates and handles a collection of thumbnails
 * animated with pre-defined transitions.
 *
 * v1.0
 * 	Initial Release
 * v1.1
 * 	Added fully working zoom, handlers.
 * 	Fixed weird zoom rotation effect (by computing shortest rotation)
 * 	Fixed Editor Inspector bug (gallery set as dirty at every frame)
 * v1.2
 *	Minor bug fix
 * 	Reuploading to Unity Asset Store...
 */

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class UniflowGallery : MonoBehaviour {

	///// DELEGATES /////

	// Delegate called when the foreground thumbnail has changed.
	public delegate void OnSelectedThumbnailUpdate( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailPrevious, UniflowThumbnail a_rThumbnailSelected );

	// Delegate called when a click occurs on the current thumbnail.
	// The delegate return value specifies if the gallery should zoom the thumbnail.
	// Return a_rGallery.zoomThumbnailOnClick to use the default behaviour.
	// If several delegates are handled, they all must return true to make the gallery zoom.
	public delegate bool OnThumbnailClickEvent( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailClicked );

	// Delegate called when a zoom-in effect on the current thumbnail starts.
	public delegate void OnThumbnailZoomInEffectStart( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailZoomedIn );

	// Delegate called when a zoom-in effect on the current thumbnail ends.
	public delegate void OnThumbnailZoomInEffectEnd( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailZoomedIn );

	// Delegate called when an zoom-out effect on the current thumbnail starts.
	public delegate void OnThumbnailZoomOutEffectStart( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailZoomedOut );

	// Delegate called when an zoom-out effect on the current thumbnail ends.
	public delegate void OnThumbnailZoomOutEffectEnd( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailZoomedOut );

	// Delegate called when a recentering event is about to start.
	// The return value specifies if Uniflow should recenter from a thumbnail to another.
	// If several delegates are handled, they all must return true to allow recentering.
	// Return true to use the default behaviour.
	public delegate bool OnRecenteringEvent( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailFrom, UniflowThumbnail a_rThumbnailTo );

	// Delegate called when the gallery performs a recentering to a thumbnail.
	// The delegate return value specifies if the gallery should avoid this recentering.
	// Return true to use the default behaviour
	// If several delegates are handled, they all must return true to make the gallery recenter.
	public delegate void OnRecenteringEffectStart( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailFrom, UniflowThumbnail a_rThumbnailTo );

	// Delegate called when the gallery has ended its recentering.
	public delegate void OnRecenteringEffectEnd( UniflowGallery a_rGallery, UniflowThumbnail a_rThumbnailSelected );

	///// STATIC / CONST VARS /////

	// The material used on thumbnail during zoom
	public static Material ms_oZoomedThumbnailMaterial;
	
	// The path of the zoom material
	public const string mc_oZoomMaterialPath = "mat_UniflowThumbnailAlwaysVisible";

	public const string mc_oThumbnailPrefabPath = "prefab_UniflowThumbnail";
	public const string mc_oBackgroundPrefabPath = "prefab_UniflowBackground";
	public const string mc_oGizmoIconPath = "icon_Uniflow.png";

	///// ENUM /////

	public enum EUniflowGalleryLayout
	{
		Horizontal,
		Vertical,
		DiagonalUpLeft,
		DiagonalUpRight,
		JukeboxLeft,
		JukeboxRight
	}

	///// PUBLIC VARS /////

	public bool zoomThumbnailOnClick = true;

	public Color ambientColor = Color.grey;

	public List<Texture2D> photoList;

	// Left
	[HideInInspector]
	public Vector3 leftFlippingPosition;
	[HideInInspector]
	public Vector3 leftFlippingRotation;
	[HideInInspector]
	public float leftFlippingScale;

	// Centered
	[HideInInspector]
	public Vector3 centeredFlippingPosition;
	[HideInInspector]
	public Vector3 centeredFlippingRotation;
	[HideInInspector]
	public float centeredFlippingScale;

	// Right
	[HideInInspector]
	public Vector3 rightFlippingPosition;
	[HideInInspector]
	public Vector3 rightFlippingRotation;
	[HideInInspector]
	public float rightFlippingScale;
	///

	///// PUBLIC SETTERS/GETTERS /////
	public int selectedThumbnailAtStart
	{
		set
		{
			int iPhotoCount = photoList.Count;
			if( iPhotoCount > 0 )
			{
				iPhotoCount = iPhotoCount - 1;
			}

			m_iSelectedThumbnailAtStart = Mathf.Clamp( value, 0, iPhotoCount );
		}
		
		get
		{
			return m_iSelectedThumbnailAtStart;
		}
	}

	public int selectedThumbnail
	{
		get
		{
			return m_iSelectedThumbnailIndex;
		}
		set
		{
			if( m_bCanScroll == true )
			{
				// Computes the new gallery position according to the thumbnail index
				int iIndexToGo = Mathf.Clamp( value, 0, m_iThumbnailCount - 1 );

				// Recenters gallery to selected thumbnail
				StartRecentering( iIndexToGo );
			}
		}
	}

	public float thumbnailRatio
	{
		set
		{
			m_fThumbnailRatio = Mathf.Clamp( value, float.Epsilon, float.MaxValue );
		}
		get
		{
			return m_fThumbnailRatio;
		}
	}

	public float thumbnailSpacing
	{
		set
		{
			m_fThumbnailSpacing = Mathf.Clamp( value, float.Epsilon, float.PositiveInfinity );
		}
		get
		{
			return m_fThumbnailSpacing;
		}
	}	

	public float planeColliderSize
	{
		get
		{
			return m_fPlaneColliderHalfSize * 2.0f;
		}
		set
		{
			m_fPlaneColliderHalfSize = Mathf.Clamp( value * 0.5f, float.Epsilon, float.PositiveInfinity );
		}
	}

	public EUniflowGalleryLayout galleryLayout
	{
		get
		{
			return m_eGalleryLayout;
		}
		set
		{
			///// Flipping reset /////
			// Position offsets
			leftFlippingPosition     = Vector3.zero;
			centeredFlippingPosition = Vector3.zero;
			rightFlippingPosition    = Vector3.zero;

			// Rotation offsets
			leftFlippingRotation     = Vector3.zero;
			centeredFlippingRotation = Vector3.zero;
			rightFlippingRotation    = Vector3.zero;

			// Scale factors
			leftFlippingScale     = 0.0f;
			centeredFlippingScale = 0.0f;
			rightFlippingScale    = 0.0f;

			switch( value )
			{
				case EUniflowGalleryLayout.Horizontal:
				{
					// Left
					leftFlippingPosition.x = -0.75f;
					leftFlippingRotation.y = -80.0f;

					// Center
					centeredFlippingPosition.z = -0.5f;

					// Right
					rightFlippingPosition.x = 0.75f;
					rightFlippingRotation.y = 80.0f;
				}
				break;

				case EUniflowGalleryLayout.Vertical:
				{
					// Left
					leftFlippingPosition.x = -0.75f;
					leftFlippingRotation.y = -80.0f;
					leftFlippingRotation.z = -90.0f;

					// Center
					centeredFlippingPosition.z = -0.5f;
					centeredFlippingRotation.z = -90.0f;

					// Right
					rightFlippingPosition.x = 0.75f;
					rightFlippingRotation.y = 80.0f;
					rightFlippingRotation.z = -90.0f;
				}
				break;

				case EUniflowGalleryLayout.DiagonalUpLeft:
				{
					// Left
					leftFlippingPosition.x = -0.75f;
					leftFlippingRotation.y = -80.0f;

					// Center
					centeredFlippingPosition.z = -0.5f;
					centeredFlippingRotation.z = -45.0f;

					// Right
					rightFlippingPosition.x = 0.75f;
					rightFlippingRotation.y = 80.0f;
				}
				break;
				case EUniflowGalleryLayout.DiagonalUpRight:
				{
					// Left
					leftFlippingPosition.x = -0.75f;
					leftFlippingRotation.y = -80.0f;

					// Center
					centeredFlippingPosition.z = -0.5f;
					centeredFlippingRotation.z = 45.0f;

					// Right
					rightFlippingPosition.x = 0.75f;
					rightFlippingRotation.y = 80.0f;		
				}
				break;
				case EUniflowGalleryLayout.JukeboxLeft:
				{
					// Left
					leftFlippingPosition.x = -0.75f;
					leftFlippingRotation.y = -90.0f;
					leftFlippingRotation.z = -270.0f;

					// Center
					centeredFlippingPosition.z = 1.5f;
					centeredFlippingRotation.y = -90.0f;

					// Right
					rightFlippingPosition.x = 0.75f;
					rightFlippingRotation.y = -90.0f;
					rightFlippingRotation.z = -270.0f;
				}
				break;
				case EUniflowGalleryLayout.JukeboxRight:
				{
					// Left
					leftFlippingPosition.x = -0.75f;
					leftFlippingRotation.y = -90.0f;
					leftFlippingRotation.z = -270.0f;

					// Center
					centeredFlippingPosition.z = -1.5f;
					centeredFlippingRotation.y = -90.0f;

					// Right
					rightFlippingPosition.x = 0.75f;
					rightFlippingRotation.y = -90.0f;
					rightFlippingRotation.z = -270.0f;
				}
				break;
			}
			m_eGalleryLayout = value;
		}
	}

	public OnSelectedThumbnailUpdate onSelectedThumbnailUpdateHandler
	{
		get
		{
			return m_dOnSelectedThumbUpdateHandler;
		}
		set
		{
			m_dOnSelectedThumbUpdateHandler = value;
		}
	}

	public OnThumbnailClickEvent onThumbnailClickEventHandler
	{
		get
		{
			return m_dOnThumbClickEventHandler;
		}
		set
		{
			m_dOnThumbClickEventHandler = value;
		}
	}

	public OnThumbnailZoomInEffectStart onThumbnailZoomInEffectStartHandler
	{
		get
		{
			return m_dOnThumbZoomInEffectStartHandler;
		}
		set
		{
			m_dOnThumbZoomInEffectStartHandler = value;
		}
	}

	public OnThumbnailZoomInEffectEnd onThumbnailZoomInEffectEndHandler
	{
		get
		{
			return m_dOnThumbZoomInEffectEndHandler;
		}
		set
		{
			m_dOnThumbZoomInEffectEndHandler = value;
		}
	}

	public OnThumbnailZoomOutEffectStart onThumbnailZoomOutEffectStartHandler
	{
		get
		{
			return m_dOnThumbZoomOutEffectStartHandler;
		}
		set
		{
			m_dOnThumbZoomOutEffectStartHandler = value;
		}	
	}

	public OnThumbnailZoomOutEffectEnd onThumbnailZoomOutEffectEndHandler
	{
		get
		{
			return m_dOnThumbZoomOutEffectEndHandler;
		}
		set
		{
			m_dOnThumbZoomOutEffectEndHandler = value;
		}
	}

	public OnRecenteringEvent onRecenteringEventHandler
	{
		get
		{
			return m_dOnRecenteringEventHandler;
		}
		set
		{
			m_dOnRecenteringEventHandler = value;
		}
	}

	public OnRecenteringEffectStart onRecenteringEffectStartHandler
	{
		get
		{
			return m_dOnRecenteringEffectStartHandler;
		}
		set
		{
			m_dOnRecenteringEffectStartHandler = value;
		}
	}

	public OnRecenteringEffectEnd onRecenteringEffectEndHandler
	{
		get
		{
			return m_dOnRecenteringEffectEndHandler;
		}
		set
		{
			m_dOnRecenteringEffectEndHandler = value;
		}
	}

	public bool isZoomed
	{
		get
		{
			return m_bZoomMode;
		}
	}

	public bool isScrolling
	{
		get
		{
			return m_bIsUserScrolling;
		}
	}

	public float flippingDuration
	{
		get
		{
			return m_fFlippingDuration;
		}
	}

	public float zoomingDuration
	{
		get
		{
			return m_fZoomingDuration;
		}
	}

	///// PRIVATE VARS /////

	[HideInInspector]
	[SerializeField]
	private float m_fPlaneColliderHalfSize = 1.5f;

	[HideInInspector]
	[SerializeField]
	private float m_fThumbnailRatio = 1.0f;

	[HideInInspector]
	[SerializeField]
	private int m_iSelectedThumbnailAtStart = 0;

	[HideInInspector]
	[SerializeField]
	private int m_iSelectedThumbnailIndex = 0;

	[HideInInspector]
	[SerializeField]
	private EUniflowGalleryLayout m_eGalleryLayout = EUniflowGalleryLayout.Horizontal;

	// Scrolling settings
	private float m_fScrollingThreshold = 7.0f;
	private float m_fMinDistanceToSwipe = 175.0f;
	private float m_fSwipeLowPassFilter = 10.0f;

	// Thumbnail settings
	[HideInInspector]
	[SerializeField]
	private float m_fThumbnailSpacing = 0.35f;

	/// Animation stuff
	// Time to recenter gallery to selected thumbnail. Constant.
	private float m_fRecenteringDuration = 0.25f;

	// Flipping animation duration
	private float m_fFlippingDuration = 0.45f;
	private float m_fZoomingDuration = 0.3f;

	/// Handlers
	private OnSelectedThumbnailUpdate m_dOnSelectedThumbUpdateHandler;
	private OnThumbnailClickEvent m_dOnThumbClickEventHandler;

	private OnThumbnailZoomInEffectStart m_dOnThumbZoomInEffectStartHandler;
	private OnThumbnailZoomInEffectEnd m_dOnThumbZoomInEffectEndHandler;

	private OnThumbnailZoomOutEffectStart m_dOnThumbZoomOutEffectStartHandler;
	private OnThumbnailZoomOutEffectEnd m_dOnThumbZoomOutEffectEndHandler;

	private OnRecenteringEvent m_dOnRecenteringEventHandler;
	private OnRecenteringEffectStart m_dOnRecenteringEffectStartHandler;
	private OnRecenteringEffectEnd m_dOnRecenteringEffectEndHandler;

	// The thumbnail components to display and manage
	private List<UniflowThumbnail> m_rThumbnails;
	private int m_iThumbnailCount;

	// UI Effect component which will recenter gallery on selected thumbnail
	private UIEffectTransformRelativeCoords m_rUIEffectRecentring;

	// Base position is the origin position of the gallery:
	// no scrolling, first thumbnail selected
	private Vector3 m_f3BasePosition;

	// The scrolling offset is added to base position in order to
	// make gallery scrolling
	private float m_fScrollingOffset = 0.0f;

	///// Gallery state /////
	// Set to true when the gallery needs to be recentred on selected thumbnail
	private bool m_bIsRecenteringPending = false;
	private bool m_bZoomMode = false;
	private bool m_bCanScroll = true;

	///// Input state /////
	// Set to true when the user makes the gallery scrolling (by mouse/touch input)
	private bool m_bIsUserScrolling = false;
	private bool m_bIsUserSwipePending = false;
	private bool m_bUserScrolledEnough = false;
	private	bool m_bUserInput = false;

	private int m_iIndexScrollingStartedOn;
	private float m_fSwipeMovement = 0.0f;
	private float m_fSwipeScrollingFactor = 0.2f;
	private float m_fMaxScrollingThresholdToNextThumbnail;

	private Vector3 m_f3FirstInputPosition;
	private Vector3 m_f3PreviousInputPosition;

	///// Gallery coords saves /////
	private Transform m_rCachedTransform;
	private float m_fCachedScaledRightMagnitude;
	private float m_fSavedThumbnailSpacing;
	private Vector3 m_f3SavedLocalBasePosition;
	private Vector3 m_f3ZoomModeWorldPosition;
	private Quaternion m_oSavedLocalRotation;
	private Quaternion m_oZoomModeWorldRotation;

	// Prefab references
	private GameObject m_rGalleryBackground;
	private GameObject m_rThumbnailPrefab;
	private GameObject m_rGalleryBackgroundPrefab;

	private Vector3 localRight
	{
		get
		{
			if( m_rCachedTransform.parent != null )
			{
				return m_rCachedTransform.parent.InverseTransformDirection( m_rCachedTransform.right ).normalized;
			}
			else
			{
				return m_rCachedTransform.right;
			}
		}
	}

	public void Awake( )
	{	
		if( ms_oZoomedThumbnailMaterial == null )
		{
			ms_oZoomedThumbnailMaterial = Resources.Load( mc_oZoomMaterialPath, typeof( Material ) ) as Material;
		}
		
		m_rCachedTransform = transform;
		m_f3BasePosition   = m_rCachedTransform.localPosition;
		m_fCachedScaledRightMagnitude = Vector3.Scale( m_rCachedTransform.localScale, Vector3.right ).magnitude;

		// Recentering component initialisation
		m_rUIEffectRecentring = gameObject.AddComponent<UIEffectTransformRelativeCoords>( ) as UIEffectTransformRelativeCoords;
		m_rUIEffectRecentring.Init( );

		// Creates a delegate which warns gallery when recentering has finished
		m_rUIEffectRecentring.effectEndedDelegate += new UIEffectTemplate.EffectEndedDelegate( this.EndOfRecenteringEffect );

		// Load thumbnail prefab
		m_rThumbnailPrefab = Resources.Load( mc_oThumbnailPrefabPath, typeof( GameObject ) ) as GameObject;

		// Thumbnails creation
		m_iThumbnailCount = photoList.Count;
		m_rThumbnails = new List<UniflowThumbnail>( );

		// Commented due to Flash Export incompatibility
		//m_rThumbnails.Capacity = m_iThumbnailCount;

		// Adds thumbnails
		for( int iPhotoIndex = 0; iPhotoIndex < m_iThumbnailCount; ++iPhotoIndex )
		{
			AddThumbnailToGallery( m_rThumbnailPrefab, iPhotoIndex );
		}

		// Gallery background Initialization
		m_rGalleryBackgroundPrefab = Resources.Load( mc_oBackgroundPrefabPath, typeof( GameObject ) ) as GameObject;
		m_rGalleryBackground = Instantiate( m_rGalleryBackgroundPrefab, Vector3.zero, m_rGalleryBackgroundPrefab.transform.localRotation ) as GameObject;

		UniflowZoomBackground rZoomBackgroundComponent = m_rGalleryBackground.GetComponent<UniflowZoomBackground>( ) as UniflowZoomBackground;
		rZoomBackgroundComponent.Init( this );
		m_rGalleryBackground.active = false;

		// Maximum distance the user have to scroll
		m_fMaxScrollingThresholdToNextThumbnail = m_rThumbnailPrefab.renderer.bounds.size.x;
	}

	// Sets initial state of the gallery, e.g. sets thumbnails transform
	public virtual void Start( )
	{
		m_f3PreviousInputPosition = Input.mousePosition;

		m_iSelectedThumbnailIndex = m_iSelectedThumbnailAtStart;

		// Adjusts gallery position to the selected thumbnail
		m_fScrollingOffset = ThumbnailIndex2ScrollingOffset( m_iSelectedThumbnailIndex );

		// Animates/sets thumbnails before selected thumbnail
		UpdateUnselectedThumbnails( 0,
		                           m_iSelectedThumbnailIndex - 1,
		                           UniflowThumbnail.EThumbnailFlippingAnimation.Left,
		                           true );
		// Animates/sets thumbnails after selected thumbnail
		UpdateUnselectedThumbnails( m_iSelectedThumbnailIndex + 1,
		                           m_iThumbnailCount - 1,
		                           UniflowThumbnail.EThumbnailFlippingAnimation.Right,
		                           true );

		// Animates/sets selected thumbnail
		UpdateSelectedThumb( m_iSelectedThumbnailIndex, true );

		m_rCachedTransform.localPosition = CurrentGalleryLocalPosition( );
	}

	// Gallery update is performed AFTER thumbnails updates
	void LateUpdate ()
	{
		Vector3 f3InputPosition = Vector3.zero;
		Vector3 f3InputMovement = Vector3.zero;

		// Computes the scrolling offset according to the platform inputs
		switch( Application.platform )
		{
			// Consoles
            //case RuntimePlatform.WiiPlayer:
			case RuntimePlatform.XBOX360:
			case RuntimePlatform.PS3:
			{
				Debug.LogWarning( "/!\\ !!! Uniflow Gallery: consoles are not supported !!! /!\\" );
			}
			break;

			// Tactil smartphones / tablets => touchscreen
			case RuntimePlatform.IPhonePlayer:
			case RuntimePlatform.Android:
			{
				if( Input.touchCount > 0 )
				{
					Touch oFirstTouch = Input.touches[ 0 ];
					if( m_bUserInput == false )
					{
						m_f3PreviousInputPosition.x = oFirstTouch.position.x;
						m_f3PreviousInputPosition.y = oFirstTouch.position.y;
						m_f3PreviousInputPosition.z = 0.0f;
					
						m_f3FirstInputPosition = m_f3PreviousInputPosition;
					}

					m_bUserInput = true;

					f3InputPosition.x = oFirstTouch.position.x;
					f3InputPosition.y = oFirstTouch.position.y;
	
					f3InputMovement = f3InputPosition - m_f3PreviousInputPosition;

					m_f3PreviousInputPosition = f3InputPosition;
				}
				else
				{
					m_bUserInput = false;
				}
			}
			break;

			// PC / Mac => mouse
			default:
			{
				if( Input.GetMouseButton( 0 ) )
				{
					if( m_bUserInput == false )
					{
						m_f3FirstInputPosition = Input.mousePosition;
					}

					m_bUserInput = true;
					f3InputPosition = Input.mousePosition;
					f3InputMovement = f3InputPosition - m_f3PreviousInputPosition;
				}
				else
				{
					m_bUserInput = false;
				}
				m_f3PreviousInputPosition = Input.mousePosition;
			}
			break;
		}

		float fScrollingMovement = ManageInputScrolling( f3InputPosition, f3InputMovement, m_bUserInput );

		ScrollGallery( fScrollingMovement );
		UpdateGallery( );
	}

	// Instantiates the given prefab as a thumbnail
	public GameObject AddThumbnailToGallery( GameObject a_rThumbnailPrefab, int a_iIndex )
	{
		// Prefab instancing
		GameObject rThumbnailGameObject = Instantiate( a_rThumbnailPrefab, Vector3.zero, Quaternion.identity ) as GameObject;

		// Retrieves Thumbnail component in the prefab, if any, creates it otherwise 
		UniflowThumbnail rThumbnailComponent = rThumbnailGameObject.GetComponent<UniflowThumbnail>( );
		if( rThumbnailComponent == null )
		{
			rThumbnailComponent = rThumbnailGameObject.AddComponent<UniflowThumbnail>( );
		}

		///// Sets transform properties. Parent and scaling are set first. /////
		rThumbnailGameObject.transform.parent = m_rCachedTransform;
		// Scale thumbnail according to specified ratio
		Vector3 f3ScaleRatio = new Vector3( m_fThumbnailRatio, 1.0f, 1.0f );
		rThumbnailGameObject.transform.localScale = Vector3.Scale( f3ScaleRatio, a_rThumbnailPrefab.transform.localScale );

		// Thumbnail placement in gallery, according to its index
		rThumbnailGameObject.transform.localPosition = ThumbnailLocalPositionFromIndex( a_iIndex );
		rThumbnailGameObject.transform.localRotation = a_rThumbnailPrefab.transform.rotation;

		// Clones the thumbnail material used in zoom mode
		Material rZoomedThumbnailMaterial = Instantiate( ms_oZoomedThumbnailMaterial ) as Material;
		rZoomedThumbnailMaterial.mainTexture = photoList[ a_iIndex ];

		// Sets photo texture to thumbnail material
		rThumbnailGameObject.renderer.material.mainTexture = photoList[ a_iIndex ];

		// Inits thumbnail component
		rThumbnailComponent.Init( this, a_iIndex, rZoomedThumbnailMaterial );

		// Finally, adds it to the gallery!
		m_rThumbnails.Add( rThumbnailComponent );

		// Desactivate thumbnail if gallery inactive or disabled (in order to follow Unity guidelines)
		if( enabled == false || gameObject.active == false )
		{
			rThumbnailGameObject.SetActiveRecursively( false );
		}

		return rThumbnailGameObject;
	}

	// Flips thumbnails in index range [ start ... end ] and sets them as "unselected"
	private void UpdateUnselectedThumbnails( int a_iStartIndex,
	                                          int a_iEndIndex,
	                                          UniflowThumbnail.EThumbnailFlippingAnimation a_eThumbnailAnimation,
	                                          bool a_bForceEndOfAnimation )
	{
		UniflowThumbnail rThumbComponent;
		for( int iThumbnailIndex = a_iStartIndex; iThumbnailIndex <= a_iEndIndex; ++iThumbnailIndex )
		{
			rThumbComponent = m_rThumbnails[ iThumbnailIndex ];
			rThumbComponent.Flip( a_eThumbnailAnimation, a_bForceEndOfAnimation );
			rThumbComponent.selected = false;
		}
	}

	// Flips given thumbnail and sets it as "selected"
	private void UpdateSelectedThumb( int a_iThumbIndex, bool a_bForceEndOfAnimation )
	{
		UniflowThumbnail rThumbComponent = m_rThumbnails[ a_iThumbIndex ];
		rThumbComponent.Flip( UniflowThumbnail.EThumbnailFlippingAnimation.Centered, a_bForceEndOfAnimation );
		rThumbComponent.selected = true;
	}

	// Computes 1:1 scrolling (even in zoom mode)
	// Sets m_bIsScrolling value accordingly
	private float ManageInputScrolling( Vector3 a_f3RawInputPosition, Vector3 a_f3RawInputMovement, bool a_bUserInput )
	{
		float fScrollingMovement = 0.0f;

		m_bIsUserScrolling = false;

		// Any user interaction?
		if( a_bUserInput == true )
		{
			Camera rMainCamera = Camera.mainCamera;

			Vector3 f3PreviousRawInputPosition = a_f3RawInputPosition - a_f3RawInputMovement;

			// Screen -> World rays
			Ray oPreviousRawInputRay = rMainCamera.ScreenPointToRay( f3PreviousRawInputPosition );
			Ray oRawInputRay = rMainCamera.ScreenPointToRay( a_f3RawInputPosition );

			// Plane computation
			Vector3 f3CameraToGalleryWorldVector = m_rCachedTransform.position - rMainCamera.transform.position;
			Vector3 f3WorldDownToGallery = Vector3.Cross( m_rCachedTransform.right, f3CameraToGalleryWorldVector );
			Vector3 f3WorldNormalPlane = Vector3.Cross( m_rCachedTransform.right, f3WorldDownToGallery );

			Plane oScrollPlane = new Plane( f3WorldNormalPlane.normalized, m_rCachedTransform.position );

			// Performs 2 raycasts from input position against computed gallery plane collider
			float fPreviousRawInputDistance;
			float fRawInputDistance;

			// Performs 2 raycasts from input positions to gallery box collider
			if( oScrollPlane.Raycast( oPreviousRawInputRay, out fPreviousRawInputDistance ) == true &&
			   oScrollPlane.Raycast( oRawInputRay, out fRawInputDistance ) == true )
			{
				// Adds movement to cumulated scrolling
				// This mecanism defines a dead/tolerance zone around user input
				// to prevent little unwanted scrollings (when clicking, typically)
				if( m_bUserScrolledEnough == false )
				{
					m_bUserScrolledEnough = Vector3.Distance( m_f3FirstInputPosition, a_f3RawInputPosition ) >= m_fScrollingThreshold;
					m_iIndexScrollingStartedOn = m_iSelectedThumbnailIndex;
				}

				// Computes hit points from ray origins + ray direction * distance from plane ( p = o + dt )
				Vector3 f3PreviousInputHitPoint = oPreviousRawInputRay.origin + oPreviousRawInputRay.direction * fPreviousRawInputDistance;
				Vector3 f3InputHitPoint = oRawInputRay.origin + oRawInputRay.direction * fRawInputDistance;

				// Projects
				Vector3 f3InputHitPointToBasePositionVector = f3InputHitPoint - m_rCachedTransform.position;
				Vector3 f3InputHitPointProjectedOnGalleryRail = m_rCachedTransform.position + Vector3.Project( f3InputHitPointToBasePositionVector, m_rCachedTransform.right );
				float fHitPointGalleryRailDistance = Vector3.Distance( f3InputHitPointProjectedOnGalleryRail, f3InputHitPoint );

				if( fHitPointGalleryRailDistance <= m_fPlaneColliderHalfSize )
				{
					// World scrolling vector
					Vector3 f3WorldScrollingMovement = f3InputHitPoint - f3PreviousInputHitPoint;
			
					// The final scrolling value is computed with the dot product
					// between the scrolling vector and the local right gallery unitary vector
					fScrollingMovement = Vector3.Dot( f3WorldScrollingMovement, m_rCachedTransform.right );

					// True if scrolling movement is above threshold
					m_bIsUserScrolling = m_bUserScrolledEnough;

					// SWIPE
					float fRawSwipeMovement = a_f3RawInputMovement.magnitude / Time.deltaTime;
					if( fRawSwipeMovement >= m_fMinDistanceToSwipe )
					{	
						m_bIsUserSwipePending = true;
						m_fSwipeMovement = fScrollingMovement / Time.deltaTime;
					}
					else if( fRawSwipeMovement >= m_fSwipeLowPassFilter ) // Low Pass filter
					{
						m_bIsUserSwipePending = false;
					}
				}
			}
		}
		else
		{
			// Resets cumulated scrolling
			m_bUserScrolledEnough = false;
		}

		return fScrollingMovement;
	}

	// Performs gallery scrolling
	// float a_fScrollingMovement: represent gallery displacement (factor of .right unitary vector)
	public void ScrollGallery( float a_fScrollingMovement )
	{
		if( m_bCanScroll == true )
		{
			if( m_bIsUserScrolling == true )
			{
	
				// Stops any recentering
				if( m_rUIEffectRecentring.isPlaying == true )
				{
					StopRecentering( );
				}
				
				// Adds user scrolling to current scrolling offset
				m_fScrollingOffset += a_fScrollingMovement;

				if( m_bZoomMode == true )
				{
					m_fScrollingOffset = Mathf.Clamp( m_fScrollingOffset, ThumbnailIndex2ScrollingOffset( m_iThumbnailCount - 1 ), 0.0f );
				}

				Vector3 f3CurrentLocalPosition = CurrentGalleryLocalPosition( );
				Vector3 f3CurrentScrollingOffsetVector = f3CurrentLocalPosition - m_f3BasePosition;

				// Sets new gallery position
				m_rCachedTransform.localPosition = f3CurrentLocalPosition;
	
				// Sets the new position where to start recentring from
				// (kind of hack, see finalPosition setter)
				m_rUIEffectRecentring.finalPositionOffset = f3CurrentScrollingOffsetVector;
				m_bIsRecenteringPending = true;
			}
			else if( m_bIsRecenteringPending == true || m_bIsUserSwipePending == true )
			{
				// Start recentering to selected thumbnail
				int iThumbnailIndexToRecenterTo = m_iSelectedThumbnailIndex;

				if( m_bIsUserSwipePending == true )
				{
					if( m_bZoomMode == true )
					{
						float fRecenteringDuration = m_fRecenteringDuration;
	
						float fCurrentScrolling = CurrentScrolling( );
						float fScrollingDifference = ThumbnailIndex2ScrollingOffset( iThumbnailIndexToRecenterTo ) - fCurrentScrolling;
	
						int iScrollingDifferenceSign = (int) Mathf.Sign( fScrollingDifference );
						int iSwipeMovementSign = (int) Mathf.Sign( m_fSwipeMovement );
	
						if( fScrollingDifference == 0.0f || iScrollingDifferenceSign != iSwipeMovementSign )
						{
							iThumbnailIndexToRecenterTo = iThumbnailIndexToRecenterTo - iSwipeMovementSign;
							iThumbnailIndexToRecenterTo = Mathf.Clamp( iThumbnailIndexToRecenterTo, 0, m_iThumbnailCount - 1 );
	
							float fDistanceToThumbnail = ThumbnailIndex2ScrollingOffset( iThumbnailIndexToRecenterTo ) - fCurrentScrolling;
				
							if( fDistanceToThumbnail != 0.0f )
							{
								float fRecenteringWithSwipeMovementDuration = Mathf.Abs( fDistanceToThumbnail / m_fSwipeMovement );
								fRecenteringDuration = Mathf.Min( fRecenteringWithSwipeMovementDuration, fRecenteringDuration );
							}
						}
	
						StartRecentering( iThumbnailIndexToRecenterTo, fRecenteringDuration, gkInterpolate.EaseType.EaseOutSine );
					}
					else
					{
						// Simulates inertia
						float fSwippedScrollingDistance = m_fSwipeMovement * m_fRecenteringDuration * m_fSwipeScrollingFactor;
						float fSwippedScrollingOffset = CurrentScrolling( ) + fSwippedScrollingDistance;
						iThumbnailIndexToRecenterTo = ScrollingOffset2ThumbnailIndex( fSwippedScrollingOffset );
	
						StartRecentering( iThumbnailIndexToRecenterTo );
					}
					m_bIsUserSwipePending = false;
					m_fSwipeMovement = 0.0f;
				}
				else
				{
					// Computing gap between two thumbnails.
					float fScrollingThresholdToNextThumbnail = m_fCachedScaledRightMagnitude * m_fThumbnailSpacing;
					float fCurrentScrolling = CurrentScrolling( );
					float fScrollingDifference = ThumbnailIndex2ScrollingOffset( m_iSelectedThumbnailIndex ) - fCurrentScrolling;

					int iScrollingDifferenceSign = (int) Mathf.Sign( fScrollingDifference );
					int iIndexDifference = m_iSelectedThumbnailIndex - m_iIndexScrollingStartedOn;
					int iIndexDifferenceSign = (int) Mathf.Sign( iIndexDifference );

					if( fScrollingThresholdToNextThumbnail > m_fMaxScrollingThresholdToNextThumbnail )
					{
						fScrollingThresholdToNextThumbnail = m_fMaxScrollingThresholdToNextThumbnail;
					}

					// The distance the user have to scroll to the next thumbnail is the half of the gap
					fScrollingThresholdToNextThumbnail = fScrollingThresholdToNextThumbnail * 0.5f;

					if( fScrollingDifference != 0 && Mathf.Abs( fScrollingDifference ) >= fScrollingThresholdToNextThumbnail )
					{
						if( iIndexDifference == 0 || iScrollingDifferenceSign == iIndexDifferenceSign )
						{
							iThumbnailIndexToRecenterTo = iThumbnailIndexToRecenterTo + iScrollingDifferenceSign;
							iThumbnailIndexToRecenterTo = Mathf.Clamp( iThumbnailIndexToRecenterTo, 0, m_iThumbnailCount - 1 );
						}
					}

					StartRecentering( iThumbnailIndexToRecenterTo );
				}
			}
		}
	}

	private void UpdateGallery( )
	{
		int iPreviousSelectedThumbnailIndex = m_iSelectedThumbnailIndex;
		float fCurrentScrolling = CurrentScrolling( );

		// Computes selected thumbnail index from current scrolling
		m_iSelectedThumbnailIndex = ScrollingOffset2ThumbnailIndex( fCurrentScrolling );			

		// Animates thumbnails if needed
		// Must consider the direction where the gallery is moving and if several thumbnails
		// may be animated simultaneously in case of the new selected thumbnail is too far away from the previous one
		if( m_iSelectedThumbnailIndex != iPreviousSelectedThumbnailIndex )
		{
			UpdateSelectedThumb( m_iSelectedThumbnailIndex, false );

			if( iPreviousSelectedThumbnailIndex < m_iSelectedThumbnailIndex )
			{
				UpdateUnselectedThumbnails( iPreviousSelectedThumbnailIndex,
				                           m_iSelectedThumbnailIndex - 1,
				                           UniflowThumbnail.EThumbnailFlippingAnimation.Left,
				                           false );
			}
			else
			{
				UpdateUnselectedThumbnails( m_iSelectedThumbnailIndex + 1,
				                           iPreviousSelectedThumbnailIndex,
				                           UniflowThumbnail.EThumbnailFlippingAnimation.Right,
				                           false );
			}

			// Warns that the selected thumbnail has been updated
			if( m_dOnSelectedThumbUpdateHandler != null )
			{
				m_dOnSelectedThumbUpdateHandler( this, m_rThumbnails[ iPreviousSelectedThumbnailIndex ], m_rThumbnails[ m_iSelectedThumbnailIndex ] );
			}
		}
	}

	/////////////// ZOOM ///////////////

	// Called when the user is zooming on a thumbnail
	public void SetupThumbnailZoom( )
	{
		bool bShouldZoom = false;

		if( m_bIsUserScrolling == false && m_rUIEffectRecentring.isPlaying == false && m_bUserScrolledEnough == false )
		{
			// Avoids calling OnThumbnailClickedEvent handlers for nothing
			bShouldZoom = ShouldZoom( );
		}

		if( bShouldZoom == true )
		{
			if( m_rUIEffectRecentring.isPlaying == true )
			{
				StopRecentering( );	
			}

			m_bIsUserSwipePending = false;

			UniflowThumbnail rSelectedThumbnailComponent = m_rThumbnails[ m_iSelectedThumbnailIndex ];

			// Enters zoom mode
			if( m_bZoomMode == false )
			{
				// Enables zoom mode
				m_bZoomMode = true;

				// Disables scrolling during zoom effect
				m_bCanScroll = false;

				// Enables background
				UniflowZoomBackground rZoomBackground = m_rGalleryBackground.GetComponent<UniflowZoomBackground>( ) as UniflowZoomBackground;
				rZoomBackground.FadeIn( );

				// Computes thumbnail position in front of camera
				Camera rMainCamera = Camera.mainCamera;

				float fScaledThumbnailHeight = m_rThumbnailPrefab.renderer.bounds.extents.y * m_rCachedTransform.localScale.y;
				float fScaledThumbnailWidth = m_rThumbnailPrefab.renderer.bounds.extents.x * m_fThumbnailRatio * m_rCachedTransform.localScale.x;

				float fDistanceFromCamera;

				// Computes distance = height / tan (FOV/2)
				// according to dominant component (width or height)
				if( rMainCamera.aspect <= m_fThumbnailRatio * m_rCachedTransform.localScale.x )
				{
					// Width case
					fDistanceFromCamera = UniflowUtils.DistanceToCameraFromFrustumWidth( rMainCamera, fScaledThumbnailWidth );
				}
				else
				{
					// Height case
					fDistanceFromCamera = UniflowUtils.DistanceToCameraFromFrustumHeight( rMainCamera, fScaledThumbnailHeight );
				}
				
				// The world position
				m_f3ZoomModeWorldPosition = rMainCamera.transform.position + rMainCamera.transform.forward * fDistanceFromCamera;

				// Set rotation as facing main camera (i.e. same rotation + 180deg.)
				m_oZoomModeWorldRotation = Quaternion.AngleAxis( 180.0f, rMainCamera.transform.up ) * rMainCamera.transform.rotation;

				// Thumbnail zoom
				rSelectedThumbnailComponent.ZoomIn( m_f3ZoomModeWorldPosition, m_oZoomModeWorldRotation );

				if( m_dOnThumbZoomInEffectStartHandler != null )
				{
					m_dOnThumbZoomInEffectStartHandler( this, rSelectedThumbnailComponent );
				}
			}
			else
			{
				// Disables zoom mode
				m_bZoomMode = false;

				// Disables scrolling (to avoid unwanted scrolling during zoom-out effect)
				m_bCanScroll = false;

				// Restores gallery settings (position, scrolling, spacing)
				m_f3BasePosition    = m_f3SavedLocalBasePosition;
				m_fThumbnailSpacing = m_fSavedThumbnailSpacing;
				m_fScrollingOffset  = ThumbnailIndex2ScrollingOffset( m_iSelectedThumbnailIndex );

				// Hides gallery background
				UniflowZoomBackground rZoomBackground = m_rGalleryBackground.GetComponent<UniflowZoomBackground>( ) as UniflowZoomBackground;
				rZoomBackground.FadeOut( );

				// Update thumbnails
				for( int iThumbnailIndex = 0; iThumbnailIndex < m_iThumbnailCount; ++iThumbnailIndex )
				{
					UniflowThumbnail rThumbnailComponent = m_rThumbnails[ iThumbnailIndex ];
	
					// Repositions thumbnail
					rThumbnailComponent.Replace( ThumbnailLocalPositionFromIndex( iThumbnailIndex ) );

					// Disables zoomed state
					rThumbnailComponent.isZoomed = false;
				}
			
				// Animates/sets thumbnails before selected thumbnail
				UpdateUnselectedThumbnails( 0,
				                           m_iSelectedThumbnailIndex - 1,
				                           UniflowThumbnail.EThumbnailFlippingAnimation.Left,
				                           true );
				// Animates/sets thumbnails after selected thumbnail
				UpdateUnselectedThumbnails( m_iSelectedThumbnailIndex + 1,
				                           m_iThumbnailCount - 1,
				                           UniflowThumbnail.EThumbnailFlippingAnimation.Right,
				                           true );

				// Animates/sets selected thumbnail
				UpdateSelectedThumb( m_iSelectedThumbnailIndex, true );

				// Repositions gallery
				m_rCachedTransform.localRotation = m_oSavedLocalRotation;
				m_rCachedTransform.localPosition = CurrentGalleryLocalPosition( );

				// 
				m_rUIEffectRecentring.startPosition = m_f3BasePosition;
				m_rUIEffectRecentring.startRotation = m_rCachedTransform.localRotation;
				m_rUIEffectRecentring.finalPositionOffset = m_fScrollingOffset * this.localRight;

				// Zooms out selected thumbnail
				rSelectedThumbnailComponent.ZoomOut( m_f3ZoomModeWorldPosition, m_oZoomModeWorldRotation );

				if( m_dOnThumbZoomOutEffectStartHandler != null )
				{
					m_dOnThumbZoomOutEffectStartHandler( this, rSelectedThumbnailComponent );
				}
			}
		}
	}

	public void PostZoomInSetup( )
	{
		if( m_bZoomMode == true )
		{
			Camera rMainCamera = Camera.mainCamera;
			UniflowThumbnail rSelectedThumbnailComponent = m_rThumbnails[ m_iSelectedThumbnailIndex ];
			Transform rThumbnailTransform = rSelectedThumbnailComponent.transform;
			rThumbnailTransform.parent = null;

			// Saves previous base position/rotation
			m_f3SavedLocalBasePosition = m_f3BasePosition;
			m_oSavedLocalRotation = m_rCachedTransform.localRotation;

			Vector3 f3ZoomVector = m_f3ZoomModeWorldPosition - ThumbnailWorldPositionFromIndex( m_iSelectedThumbnailIndex );

			if( m_rCachedTransform.parent != null )
			{
				m_f3BasePosition += m_rCachedTransform.parent.InverseTransformDirection( f3ZoomVector );
			}
			else
			{
				// Both in world coords => just add
				m_f3BasePosition += f3ZoomVector;				
			}

			m_rCachedTransform.localRotation = Quaternion.AngleAxis( 180.0f, rThumbnailTransform.up ) * rThumbnailTransform.localRotation;
			m_rCachedTransform.localPosition = CurrentGalleryLocalPosition( );
			rThumbnailTransform.parent = m_rCachedTransform;

			// Saves current spacing
			m_fSavedThumbnailSpacing = m_fThumbnailSpacing;

			if( rMainCamera.aspect <= m_fThumbnailRatio * m_rCachedTransform.localScale.x )
			{
				// Wider than screen
				float fScaledThumbnailWidth = m_rThumbnailPrefab.renderer.bounds.extents.x * m_fThumbnailRatio;
				m_fThumbnailSpacing = 2.0f * fScaledThumbnailWidth;
			}
			else
			{
				// Taller than screen
				// Need to compute screen width in world coord.
				float fTanHorizontalFOV = Mathf.Tan( UniflowUtils.HorizontalFOV( rMainCamera ) * 0.5f );
				float fScaledThumbnailHeight = m_rThumbnailPrefab.renderer.bounds.extents.y * m_rCachedTransform.localScale.y;
				float fDistance = UniflowUtils.DistanceToCameraFromFrustumHeight( rMainCamera, fScaledThumbnailHeight );
			
				m_fThumbnailSpacing = 2.0f * ( ( fTanHorizontalFOV * fDistance ) / m_fCachedScaledRightMagnitude );
			}

			m_fScrollingOffset = ThumbnailIndex2ScrollingOffset( m_iSelectedThumbnailIndex );
			
			for( int iThumbnailIndex = 0; iThumbnailIndex < m_iThumbnailCount; ++iThumbnailIndex )
			{
				UniflowThumbnail rThumbnailComponent = m_rThumbnails[ iThumbnailIndex ];
		
				rThumbnailComponent.Flip( UniflowThumbnail.EThumbnailFlippingAnimation.Zoom, true );
				rThumbnailComponent.Replace( ThumbnailLocalPositionFromIndex( iThumbnailIndex ) );

				rThumbnailComponent.isZoomed = true;
			}
			m_bCanScroll = true;

			m_rCachedTransform.localPosition = CurrentGalleryLocalPosition( );

			m_rUIEffectRecentring.startPosition = m_f3BasePosition;
			m_rUIEffectRecentring.startRotation = m_rCachedTransform.localRotation;
			m_rUIEffectRecentring.finalPositionOffset = m_fScrollingOffset * this.localRight;

			if( m_dOnThumbZoomInEffectEndHandler != null )
			{
				m_dOnThumbZoomInEffectEndHandler( this, rSelectedThumbnailComponent );
			}
		}
	}

	public void PostZoomOutSetup( )
	{
		// Animates/sets selected thumbnail
		m_bCanScroll = true;
		if( m_dOnThumbZoomOutEffectEndHandler != null )
		{
			m_dOnThumbZoomOutEffectEndHandler( this, m_rThumbnails[ m_iSelectedThumbnailIndex ] );
		}
	}

	private void ToggleGalleryBackground( bool a_bEnable )
	{
		if( a_bEnable == true )
		{
			Vector3 f3LastThumbnailPosition = ThumbnailLocalPositionFromIndex( m_iThumbnailCount - 1 );
			Vector3 f3GalleryCenter = f3LastThumbnailPosition * 0.5f;
			f3GalleryCenter.z = f3GalleryCenter.z + 0.1f;

			Vector3 f3GalleryBackgroundScale = Vector3.one;
			f3GalleryBackgroundScale.x = m_fThumbnailRatio * m_iThumbnailCount;

			m_rGalleryBackground.transform.localPosition = f3GalleryCenter;
			m_rGalleryBackground.transform.localScale = f3GalleryBackgroundScale;

			m_rGalleryBackground.active = true;
		}
		else
		{
			m_rGalleryBackground.active = false;
		}
	}

	private bool ShouldZoom( )
	{
		bool bShouldZoom = zoomThumbnailOnClick;

		if( m_dOnThumbClickEventHandler != null )
		{
			bShouldZoom = true;
			UniflowThumbnail rSelectedThumbnailComponent = m_rThumbnails[ m_iSelectedThumbnailIndex ];

			foreach( OnThumbnailClickEvent dThumbnailClickEventHandler in m_dOnThumbClickEventHandler.GetInvocationList( ) )
			{
				bool bHandlerValue = dThumbnailClickEventHandler( this, rSelectedThumbnailComponent );

				bShouldZoom = bShouldZoom && bHandlerValue;
			}
		}

		return bShouldZoom;
	}

	/////////////// RECENTRING ///////////////

	// Configs & starts UIEffect component to perform a recentering to
	// the given thumbnail
	private void StartRecentering( int a_iThumbnailIndex )
	{
		StartRecentering( a_iThumbnailIndex, m_fRecenteringDuration, gkInterpolate.EaseType.EaseOutCirc );
	}

	private void StartRecentering( int a_iThumbnailIndex, float a_fRecenteringDuration, gkInterpolate.EaseType a_eEasing )
	{
		if( ShouldRecenter( a_iThumbnailIndex ) == true )
		{
			if( m_rUIEffectRecentring.isPlaying == true )
			{
				// Sets scrolling offset to current recentering scrolling
				m_fScrollingOffset = CurrentScrolling( );
			}
	
			// Computes scrolling offset
			float fRecenteringScrollingOffset = ThumbnailIndex2ScrollingOffset( a_iThumbnailIndex );
		
			m_rUIEffectRecentring.Pause( );
	
			// HACK: force previous position offset update... *sigh*
			m_rUIEffectRecentring.time = m_rUIEffectRecentring.duration;
			m_rUIEffectRecentring.finalPositionOffset = m_fScrollingOffset * this.localRight;
			m_rUIEffectRecentring.time = m_rUIEffectRecentring.duration;
			///
	
			m_rUIEffectRecentring.finalPositionOffset = fRecenteringScrollingOffset * this.localRight;
			m_rUIEffectRecentring.duration = a_fRecenteringDuration;
			m_rUIEffectRecentring.easing = a_eEasing;
			m_rUIEffectRecentring.Restart( );
	
			if( m_dOnRecenteringEffectStartHandler != null )
			{
				m_dOnRecenteringEffectStartHandler( this, m_rThumbnails[ m_iSelectedThumbnailIndex ], m_rThumbnails[ a_iThumbnailIndex ] );
			}
		}

		// Recentering no more pending
		m_bIsRecenteringPending = false;
	}

	// Stops recentering and sets scrolling offset to current recentering
	private void StopRecentering( )
	{
		m_fScrollingOffset = CurrentScrolling( );
		m_rUIEffectRecentring.Pause( );
		m_bIsRecenteringPending = false;

		if( m_dOnRecenteringEffectEndHandler != null )
		{
			m_dOnRecenteringEffectEndHandler( this, m_rThumbnails[ m_iSelectedThumbnailIndex ] );
		}
	}

	// Called when recentering has ended
	// Sets scrolling offset ot final recentering
	private void EndOfRecenteringEffect( bool a_bEffectPlayedForward )
	{
		m_bIsRecenteringPending = false;
		m_fScrollingOffset = CurrentScrolling( );

		if( m_dOnRecenteringEffectEndHandler != null )
		{
			m_dOnRecenteringEffectEndHandler( this, m_rThumbnails[ m_iSelectedThumbnailIndex ] );
		}
	}

	private bool ShouldRecenter( int a_iThumbnailIndex )
	{
		bool bShouldRecenter = true;

		if( m_dOnRecenteringEventHandler != null )
		{
			UniflowThumbnail rFromThumbnailComponent = m_rThumbnails[ m_iSelectedThumbnailIndex ];
			UniflowThumbnail rToThumbnailComponent = m_rThumbnails[ a_iThumbnailIndex ];

			foreach( OnRecenteringEvent dRecenteringEventHandler in m_dOnRecenteringEventHandler.GetInvocationList( ) )
			{
				bool bHandlerValue = dRecenteringEventHandler( this, rFromThumbnailComponent, rToThumbnailComponent );
				bShouldRecenter = bShouldRecenter && bHandlerValue;
			}
		}

		return bShouldRecenter;
	}

	/////////////// UTILS ///////////////
	// Index / scrolling / positions computations

	private float CurrentThumbnailFloatIndex( )
	{
		float fCurrentScrollingOffset = CurrentScrolling( );
		float fScaledSpacing = - m_fThumbnailSpacing * m_fCachedScaledRightMagnitude;
		return fCurrentScrollingOffset / fScaledSpacing;
	}

	private Vector3 ThumbnailLocalPositionFromIndex( int a_iThumbnailIndex )
	{
		return a_iThumbnailIndex * m_fThumbnailSpacing * Vector3.right;
	}

	private Vector3 ThumbnailWorldPositionFromIndex( int a_iThumbnailIndex )
	{
		return m_rCachedTransform.localToWorldMatrix.MultiplyPoint( ThumbnailLocalPositionFromIndex( a_iThumbnailIndex ) );
	}

	private Vector3 CurrentGalleryLocalPosition( )
	{
		return m_f3BasePosition + m_fScrollingOffset * this.localRight;
	}

	// Computes the selected thumbnail index from given scrolling offset
	private int ScrollingOffset2ThumbnailIndex( float a_fScrollingOffset )
	{
		// Checks if the gallery is currently scrolled further than its origin
		if( a_fScrollingOffset < 0.0f )
		{
			float fScaledSpacing = - m_fThumbnailSpacing * m_fCachedScaledRightMagnitude;
			float fScrolling = a_fScrollingOffset + ( fScaledSpacing * 0.5f );

			return Mathf.Clamp( Mathf.FloorToInt( fScrolling / fScaledSpacing ), 0, m_iThumbnailCount - 1 );
		}
		else
		{
			return 0;
		}
	}

	// Computes scrolling offset from given thumbnail index
	private float ThumbnailIndex2ScrollingOffset( int a_iThumbnailIndex )
	{
		return - a_iThumbnailIndex * m_fThumbnailSpacing * m_fCachedScaledRightMagnitude;
	}

	// Computes current scrolling offset
	// Always in range [-infinity .. 0.0]
	private float CurrentScrolling( )
	{
		Vector3 f3ScrollingVector = m_rCachedTransform.localPosition - m_f3BasePosition;
		return Mathf.Clamp( Vector3.Dot( this.localRight, f3ScrollingVector ), float.NegativeInfinity, 0.0f );
	}

	/////////////// EDITOR ///////////////
	// Draws plane collider limits in editor
	private void OnDrawGizmos( )
	{
		Camera rMainCamera = Camera.mainCamera;

		Vector3 f3WorldPosition = transform.position;
		Vector3 f3WorldRight = transform.right;

		Vector3 f3CameraToGalleryWorldVector = f3WorldPosition - rMainCamera.transform.position;
		Vector3 f3WorldLineOrigin = f3WorldPosition + Vector3.Project( -f3CameraToGalleryWorldVector, f3WorldRight );

		Vector3 f3WorldUpToGallery = Vector3.Cross( f3WorldRight, f3CameraToGalleryWorldVector );
		f3WorldUpToGallery = f3WorldUpToGallery.normalized;

		Vector3 f3WorldPointOnMaxPlaneLimit = f3WorldLineOrigin + m_fPlaneColliderHalfSize * f3WorldUpToGallery;
		Vector3 f3WorldPointOnMinPlaneLimit = f3WorldLineOrigin - m_fPlaneColliderHalfSize * f3WorldUpToGallery;


		///// Draws Gizmos /////

		// Plane limits
		Gizmos.color = Color.green;
		Gizmos.DrawLine( f3WorldPointOnMinPlaneLimit - 5.0f * f3WorldRight, f3WorldPointOnMinPlaneLimit + 5.0f * f3WorldRight );
		Gizmos.DrawLine( f3WorldPointOnMaxPlaneLimit - 5.0f * f3WorldRight, f3WorldPointOnMaxPlaneLimit + 5.0f * f3WorldRight );

		// Draws gallery origin
		Vector3 f3CubeScale = transform.localScale;
		f3CubeScale.x *= m_fThumbnailRatio;
		f3CubeScale.y *= 1.0f;
		f3CubeScale.z *= 0.1f;

		Gizmos.matrix = transform.localToWorldMatrix;

		Gizmos.color = Color.red;
		Gizmos.DrawWireCube( Vector3.zero, f3CubeScale );
	}
}
